你的資料在叢集外面——在 NAS、在物件儲存、在某台機器上。
而 workbench、pipeline、模型服務都在叢集裡面。
Connection 就是中間那份「怎麼連過去」的設定:位址加憑證。
在 OpenShift AI 裡它不是一個 CRD,是一個貼了特定 label 的 Secret:
oc get crd | grep -i connection
# (沒有東西)
知道這件事很重要,因為它決定了你怎麼建、怎麼查、怎麼除錯。
第一次要讓叢集裡的東西讀到叢集外的資料時。 通常依序發生:
第 3 點通常是逼你走到這一步的那個:模型換一次就重 build 一次 image,很快受不了。
ODH 內建三種(3.5 和 3.6 都是這三種):
oc get cm -n opendatahub -l opendatahub.io/connection-type=true
# oci-v1 ← OCI registry
# s3 ← S3 相容物件儲存
# uri-v1 ← 單一 URI
s3 這型要填哪些欄位,定義就在那個 ConfigMap 裡:
oc get cm s3 -n opendatahub -o jsonpath='{.data.fields}' \
| jq -r '.[] | "\(.envVar)\trequired=\(.required)"'
AWS_ACCESS_KEY_ID required=true
AWS_SECRET_ACCESS_KEY required=true
AWS_S3_ENDPOINT required=true
AWS_DEFAULT_REGION required=false
AWS_S3_BUCKET required=false
三個必填。 這比翻文件快,而且不會過期——定義就在你的叢集上。
apiVersion: v1
kind: Secret
metadata:
name: minio-connection
namespace: llm-serve-demo
labels:
opendatahub.io/dashboard: "true" # ← 沒有這個,dashboard 看不到
opendatahub.io/managed: "true"
annotations:
opendatahub.io/connection-type-ref: s3 # ← 指向上面那個型別
openshift.io/display-name: "MinIO(模型與 pipeline 產物)"
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: <key>
AWS_SECRET_ACCESS_KEY: <secret>
AWS_S3_ENDPOINT: http://minio.example:9000
AWS_DEFAULT_REGION: us-east-1
AWS_S3_BUCKET: models
也可以在 dashboard 上按「Create connection」用表單填,結果一樣。
驗證這一步:
oc get secret -n <ns> -l opendatahub.io/dashboard=true
# minio-connection
⚠️ 這只證明「建出來了」,不證明「能用」。 真正的驗證在步驟四。
同一份 Connection,三種消費方式不一樣:
spec:
template:
spec:
containers:
- name: my-workbench
envFrom:
- secretRef: { name: minio-connection } # ← 一行搞定
進去之後直接讀環境變數:
import os, boto3
s3 = boto3.client("s3",
endpoint_url=os.environ["AWS_S3_ENDPOINT"],
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"])
from kfp import kubernetes
kubernetes.use_secret_as_env(
task, secret_name="minio-connection",
secret_key_to_env={"AWS_ACCESS_KEY_ID": "AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY": "AWS_SECRET_ACCESS_KEY"})
憑證不要進 image。 image 會進 registry、會被複製、會被掃描工具展開——
任何進了 image layer 的東西都要當成已經公開。
⚠️ KServe 用的是另一組慣例——不是 label,是貼在 Secret 上的 annotation:
apiVersion: v1
kind: Secret
metadata:
name: minio-s3
namespace: llm-serve-demo
annotations:
serving.kserve.io/s3-endpoint: minio.example:9000 # ← 不含 http://
serving.kserve.io/s3-usehttps: "0"
serving.kserve.io/s3-region: us-east-1
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: <key>
AWS_SECRET_ACCESS_KEY: <secret>
但光有這個 Secret 沒用。 KServe 不會去掃 namespace 裡的 Secret,
它是從 InferenceService 用的那個 ServiceAccount 上去找:
apiVersion: v1
kind: ServiceAccount
metadata:
name: llm-sa
secrets:
- name: minio-s3 # ← 這一行才是接線
# InferenceService
spec:
predictor:
serviceAccountName: llm-sa # ← 指到上面那個 SA
model:
storageUri: s3://models/sklearn-demo/ # ← 要抓的路徑
這條鏈少一環就抓不到權重,而錯誤訊息只會說連不到 endpoint,
不會告訴你是 SA 沒掛 Secret。串起來是:
InferenceService → serviceAccountName → ServiceAccount.secrets → 有 annotation 的 Secret
所以同一個叢集上,S3 設定有三套並存的慣例:
dashboard 讀 label、KServe 讀 annotation + ServiceAccount、你的程式讀 data key。我自己的 lab 最後是開兩份 Secret:
minio-connection(給 dashboard 和 workbench,
五個欄位齊全)、minio-s3(給 KServe,只有 access key 兩個欄位 + annotation;
Day 9 的 pipeline server 也是指到這一份)。
一份 Secret 要同時滿足三套當然做得到——貼齊 label、annotation、五個 key,再掛上 SA——
但拆兩份的好處是給 KServe 的那份只帶它需要的兩個欄位。
代價是盤點時要記得有兩份,這正是 Day 6 講的
「Connections 頁不是憑證的唯一來源」。
這一步不能省。 dashboard 顯示一份 connection 存在,
只代表格式正確——label 有、必填欄位填了。
它不會拿那組憑證去連一次。
我自己就踩過:憑證填錯了,dashboard 上一切正常,
直到在 workbench 裡真的去用才拿到 SignatureDoesNotMatch。
最快的驗證方式——在 workbench 裡跑三行:
import os, boto3
s3 = boto3.client("s3", endpoint_url=os.environ["AWS_S3_ENDPOINT"],
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"])
print([o["Key"] for o in s3.list_objects_v2(Bucket=os.environ["AWS_S3_BUCKET"]).get("Contents", [])][:5])
列得出東西才算設好。
沒有 workbench 的話,用一個一次性的 pod 也行。
⚠️ oc run 沒有 --env-from(只有 --env,要一個一個列,
等於把金鑰打在指令列上留進 shell 紀錄)。整包注入要用 --overrides:
oc run s3check --rm -i --restart=Never -n <ns> --image=quay.io/minio/mc \
--overrides='{"spec":{"containers":[{"name":"s3check","image":"quay.io/minio/mc",
"command":["sh","-c","mc alias set t $AWS_S3_ENDPOINT $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY >/dev/null && mc ls t/$AWS_S3_BUCKET"],
"envFrom":[{"secretRef":{"name":"minio-connection"}}]}]}}'
跑起來會直接把 bucket 內容列出來:
[2026-08-30 14:34:03 UTC] 2.8KiB STANDARD registry.json
[2026-09-05 09:20:19 UTC] 0B llm/
[2026-09-05 09:20:19 UTC] 0B sklearn-demo/
pod "s3check" deleted
憑證從頭到尾只存在於 Secret 和 pod 裡,沒有進過你的指令列。
| 檢查 | 指令 | |
|---|---|---|
| 1 | Secret 建出來了 | oc get secret -l opendatahub.io/dashboard=true |
| 2 | dashboard 看得到 | 打開 Connections 分頁 |
| 3 | 憑證真的能連 | 上面那三行 python |
| 4 | 誰在用它 | Connections 頁的 Connected resources 欄 |
第 4 項容易被忽略但很有用:如果那一欄是 --,代表沒有東西在用這份 connection。
而如果同時你的 pipeline 又在跑,那就表示它用的是另一份你在 UI 上看不到的憑證。
Q:pipeline 明明在用 S3,dashboard 上卻沒有 connection。
A:那份 Secret 沒貼 opendatahub.io/dashboard label。
執行期讀的是 Secret 的內容,dashboard 讀的是 label——兩邊看的是同一個物件的不同部位。
盤點的時候要看實際掛進 pod 的:
oc get pods -n <ns> -o json | jq -r '
.items[].spec.containers[].envFrom[]?.secretRef.name,
.items[].spec.volumes[]?.secret.secretName
| select(. != null)' | sort -u
⚠️ 這行也抓不到 KServe 走 ServiceAccount 那條路的憑證——
那份 Secret 不會出現在 pod spec 裡。SA 那邊要另外查:
oc get sa -n <ns> -o json | jq -r '.items[] | select(.secrets) |
"\(.metadata.name) -> \(.secrets[].name)"'
Q:SignatureDoesNotMatch。
A:帳號密碼錯。注意 AWS_S3_ENDPOINT 有沒有多寫或少寫 http://——
boto3 要完整 URL,KServe 的 annotation 不要 scheme。
Q:改了 Secret,但服務還是用舊的。
A:環境變數是 pod 啟動時注入的,改 Secret 不會自動生效,要重建 pod:oc delete pod -l <你的 selector>。
Q:可以用 PVC 取代嗎?
A:可以,資料很大或本來就在企業儲存上時更合適。
但 PVC 掛進來的東西不受平台版本控制——別人改了那個檔案,
你的訓練結果就變了,而且沒有紀錄。
opendatahub-operator.v3.5.0(即 RHOAI 3.x 的上游開源版)、cert-manager-operator.v1.20.0(3.x 的必要相依,2.x 不需要)3.6.0-ea.1,kserve、aipipelines、dashboard、workbenches、modelregistry
⚠️ ODH ≠ RHOAI:元件同源,但 namespace 與部分名稱不同
(這裡是 opendatahub,商用版是 redhat-ods-*)。指令邏輯可照用,字串要自己對一次。
你們的訓練資料和模型放哪?S3、PVC,還是 NFS? 留言或到原文留言都可以,我會回。